package socks;

import java.net.*;
import java.io.*;

/**
Abstract class Proxy, base for classes Socks4Proxy and Socks5Proxy.
Defines methods for specifying default proxy, to be
used by all classes of this package.
 */
public abstract class Proxy
{

//Data members
    protected InetRange directHosts = new InetRange();
    protected InetAddress proxyIP = null;
    protected String proxyHost = null;
    protected int proxyPort;
    protected Socket proxySocket = null;
    protected InputStream in;
    protected OutputStream out;
    protected int version;
    protected Proxy chainProxy = null;
//Protected static/class variables
    protected static Proxy defaultProxy = null;

//Constructors
//====================
    Proxy(Proxy chainProxy, String proxyHost, int proxyPort) throws UnknownHostException
    {
        this.chainProxy = chainProxy;
        this.proxyHost = proxyHost;

        if (chainProxy == null)
        {
            this.proxyIP = InetAddress.getByName(proxyHost);
        }

        this.proxyPort = proxyPort;
    }

    Proxy(String proxyHost, int proxyPort) throws UnknownHostException
    {
        this(null, proxyHost, proxyPort);
    }

    Proxy(Proxy chainProxy, InetAddress proxyIP, int proxyPort)
    {
        this.chainProxy = chainProxy;
        this.proxyIP = proxyIP;
        this.proxyPort = proxyPort;
    }

    Proxy(InetAddress proxyIP, int proxyPort)
    {
        this(null, proxyIP, proxyPort);
    }

    Proxy(Proxy p)
    {
        this.proxyIP = p.proxyIP;
        this.proxyPort = p.proxyPort;
        this.version = p.version;
        this.directHosts = p.directHosts;
    }

//Public instance methods
//========================
    /**
    Get the port on which proxy server is running.
     * @return Proxy port.
     */
    public int getPort()
    {
        return proxyPort;
    }

    /**
    Get the ip address of the proxy server host.
     * @return Proxy InetAddress.
     */
    public InetAddress getInetAddress()
    {
        return proxyIP;
    }

    /**
     * Adds given ip to the list of direct addresses.
     * This machine will be accessed without using proxy.
     */
    public void addDirect(InetAddress ip)
    {
        directHosts.add(ip);
    }

    /**
     * Adds host to the list of direct addresses.
     * This machine will be accessed without using proxy.
     */
    public boolean addDirect(String host)
    {
        return directHosts.add(host);
    }

    /**
     * Adds given range of addresses to the lsit of direct addresses,
     * machines within this range will be accessed without using proxy.
     */
    public void addDirect(InetAddress from, InetAddress to)
    {
        directHosts.add(from, to);
    }

    /**
     * Sets given InetRange as the list of direct address, previous
     * list will be discarded, any changes done previously with
     * addDirect(Inetaddress) will be lost.
     * The machines in this range will be accessed without using proxy.
     * @param ir InetRange which should be used to look up direct addresses.
     * @see InetRange
     */
    public void setDirect(InetRange ir)
    {
        directHosts = ir;
    }

    /**
    Get the list of direct hosts.
     * @return Current range of direct address as InetRange object.
     * @see InetRange
     */
    public InetRange getDirect()
    {
        return directHosts;
    }

    /**
    Check wether the given host is on the list of direct address.
    @param host Host name to check.
     * @return true if the given host is specified as the direct addresses.
     */
    public boolean isDirect(String host)
    {
        return directHosts.contains(host);
    }

    /**
    Check wether the given host is on the list of direct addresses.
    @param host Host address to check.
     * @return true if the given host is specified as the direct address.
     */
    public boolean isDirect(InetAddress host)
    {
        return directHosts.contains(host);
    }

    /**
    Set the proxy which should be used to connect to given proxy.
    @param chainProxy Proxy to use to connect to this proxy.
     */
    public void setChainProxy(Proxy chainProxy)
    {
        this.chainProxy = chainProxy;
    }

    /**
    Get proxy which is used to connect to this proxy.
    @return Proxy which is used to connect to this proxy, or null
    if proxy is to be contacted directly.
     */
    public Proxy getChainProxy()
    {
        return chainProxy;
    }

    /**
    Get string representation of this proxy.
     * @returns string in the form:proxyHost:proxyPort \t Version versionNumber
     */
    public String toString()
    {
        return ("" + proxyIP.getHostName() + ":" + proxyPort + "\tVersion " + version);
    }

//Public Static(Class) Methods
//==============================
    /**
     * Sets SOCKS4 proxy as default.
    @param hostName Host name on which SOCKS4 server is running.
    @param port Port on which SOCKS4 server is running.
    @param user Username to use for communications with proxy.
     */
    public static void setDefaultProxy(String hostName, int port, String user) throws UnknownHostException
    {
        defaultProxy = new Socks4Proxy(hostName, port, user);
    }

    /**
     * Sets SOCKS4 proxy as default.
    @param ipAddress Host address on which SOCKS4 server is running.
    @param port Port on which SOCKS4 server is running.
    @param user Username to use for communications with proxy.
     */
    public static void setDefaultProxy(InetAddress ipAddress, int port, String user)
    {
        defaultProxy = new Socks4Proxy(ipAddress, port, user);
    }

    /**
     * Sets SOCKS5 proxy as default.
     * Default proxy only supports no-authentication.
    @param hostName Host name on which SOCKS5 server is running.
    @param port Port on which SOCKS5 server is running.
     */
    public static void setDefaultProxy(String hostName, int port) throws UnknownHostException
    {
        defaultProxy = new Socks5Proxy(hostName, port);
    }

    /**
     * Sets SOCKS5 proxy as default.
     * Default proxy only supports no-authentication.
    @param ipAddress Host address on which SOCKS5 server is running.
    @param port Port on which SOCKS5 server is running.
     */
    public static void setDefaultProxy(InetAddress ipAddress, int port)
    {
        defaultProxy = new Socks5Proxy(ipAddress, port);
    }

    /**
     * Sets default proxy.
    @param p Proxy to use as default proxy.
     */
    public static void setDefaultProxy(Proxy p)
    {
        defaultProxy = p;
    }

    /**
    Get current default proxy.
     * @return Current default proxy, or null if none is set.
     */
    public static Proxy getDefaultProxy()
    {
        return defaultProxy;
    }

    /**
    Parses strings in the form: host[:port:user:password], and creates
    proxy from information obtained from parsing.
    <p>
    Defaults: port = 1080.<br>
    If user specified but not password, creates Socks4Proxy, if user
    not specified creates Socks5Proxy, if both user and password are
    speciefied creates Socks5Proxy with user/password authentication.
    @param proxy_entry String in the form host[:port:user:password]
    @return Proxy created from the string, null if entry was somehow
    invalid(host unknown for example, or empty string)
     */
    public static Proxy parseProxy(String proxy_entry)
    {

        String proxy_host;
        int proxy_port = 1080;
        String proxy_user = null;
        String proxy_password = null;
        Proxy proxy;

        java.util.StringTokenizer st = new java.util.StringTokenizer(
            proxy_entry, ":");
        if (st.countTokens() < 1)
        {
            return null;
        }

        proxy_host = st.nextToken();
        if (st.hasMoreTokens())
        {
            try
            {
                proxy_port = Integer.parseInt(st.nextToken().trim());
            }
            catch (NumberFormatException nfe)
            {
            }
        }

        if (st.hasMoreTokens())
        {
            proxy_user = st.nextToken();
        }

        if (st.hasMoreTokens())
        {
            proxy_password = st.nextToken();
        }

        try
        {
            if (proxy_user == null)
            {
                proxy = new Socks5Proxy(proxy_host, proxy_port);
            }
            else if (proxy_password == null)
            {
                proxy = new Socks4Proxy(proxy_host, proxy_port, proxy_user);
            }
            else
            {
                proxy = new Socks5Proxy(proxy_host, proxy_port);
                UserPasswordAuthentication upa = new UserPasswordAuthentication(
                    proxy_user, proxy_password);

                ((Socks5Proxy) proxy).setAuthenticationMethod(upa.METHOD_ID, upa);
            }
        }
        catch (UnknownHostException uhe)
        {
            return null;
        }

        return proxy;
    }

//Protected Methods
//=================
    protected void startSession() throws SocksException
    {
        try
        {
            if (chainProxy == null)
            {
                proxySocket = new Socket(proxyIP, proxyPort);
            }
            else if (proxyIP != null)
            {
                proxySocket = new SocksSocket(chainProxy, proxyIP, proxyPort);
            }
            else
            {
                proxySocket = new SocksSocket(chainProxy, proxyHost, proxyPort);
            }

            in = proxySocket.getInputStream();
            out = proxySocket.getOutputStream();
        }
        catch (SocksException se)
        {
            throw se;
        }
        catch (IOException io_ex)
        {
            throw new SocksException(SOCKS_PROXY_IO_ERROR, "" + io_ex);
        }
    }

    protected abstract Proxy copy();

    protected abstract ProxyMessage formMessage(int cmd, InetAddress ip, int port);

    protected abstract ProxyMessage formMessage(int cmd, String host, int port) throws UnknownHostException;

    protected abstract ProxyMessage formMessage(InputStream in) throws SocksException, IOException;

    protected ProxyMessage connect(InetAddress ip, int port) throws SocksException
    {
        try
        {
            startSession();
            ProxyMessage request = formMessage(SOCKS_CMD_CONNECT, ip, port);
            return exchange(request);
        }
        catch (SocksException se)
        {
            endSession();
            throw se;
        }
    }

    protected ProxyMessage connect(String host, int port) throws UnknownHostException, SocksException
    {
        try
        {
            startSession();
            ProxyMessage request = formMessage(SOCKS_CMD_CONNECT, host, port);
            return exchange(request);
        }
        catch (SocksException se)
        {
            endSession();
            throw se;
        }
    }

    protected ProxyMessage bind(InetAddress ip, int port) throws SocksException
    {
        try
        {
            startSession();
            ProxyMessage request = formMessage(SOCKS_CMD_BIND, ip, port);
            return exchange(request);
        }
        catch (SocksException se)
        {
            endSession();
            throw se;
        }
    }

    protected ProxyMessage bind(String host, int port)
        throws UnknownHostException, SocksException
    {
        try
        {
            startSession();
            ProxyMessage request = formMessage(SOCKS_CMD_BIND, host, port);
            return exchange(request);
        }
        catch (SocksException se)
        {
            endSession();
            throw se;
        }
    }

    protected ProxyMessage accept() throws IOException, SocksException
    {
        ProxyMessage msg;
        try
        {
            msg = formMessage(in);
        }
        catch (InterruptedIOException iioe)
        {
            throw iioe;
        }
        catch (IOException io_ex)
        {
            endSession();
            throw new SocksException(SOCKS_PROXY_IO_ERROR, "While Trying accept:"
                + io_ex);
        }
        return msg;
    }

    protected ProxyMessage udpAssociate(InetAddress ip, int port) throws SocksException
    {
        try
        {
            startSession();
            ProxyMessage request = formMessage(SOCKS_CMD_UDP_ASSOCIATE, ip, port);
            if (request != null)
            {
                return exchange(request);
            }
        }
        catch (SocksException se)
        {
            endSession();
            throw se;
        }
        //Only get here if request was null
        endSession();
        throw new SocksException(SOCKS_METHOD_NOTSUPPORTED, "This version of proxy does not support UDP associate, use version 5");
    }

    protected ProxyMessage udpAssociate(String host, int port) throws UnknownHostException, SocksException
    {
        try
        {
            startSession();
            ProxyMessage request = formMessage(SOCKS_CMD_UDP_ASSOCIATE, host, port);
            if (request != null)
            {
                return exchange(request);
            }
        }
        catch (SocksException se)
        {
            endSession();
            throw se;
        }
        //Only get here if request was null
        endSession();
        throw new SocksException(SOCKS_METHOD_NOTSUPPORTED, "This version of proxy does not support UDP associate, use version 5");
    }

    protected void endSession()
    {
        try
        {
            if (proxySocket != null)
            {
                proxySocket.close();
            }
            proxySocket = null;
        }
        catch (IOException io_ex)
        {
        }
    }

    /**
     *Sends the request to SOCKS server
     */
    protected void sendMsg(ProxyMessage msg) throws SocksException, IOException
    {
        msg.write(out);
    }

    /**
     * Reads the reply from the SOCKS server
     */
    protected ProxyMessage readMsg() throws SocksException, IOException
    {
        return formMessage(in);
    }

    /**
     *Sends the request reads reply and returns it
     *throws exception if something wrong with IO
     *or the reply code is not zero
     */
    protected ProxyMessage exchange(ProxyMessage request) throws SocksException
    {
        ProxyMessage reply;
        try
        {
            request.write(out);
            reply = formMessage(in);
        }
        catch (SocksException s_ex)
        {
            throw s_ex;
        }
        catch (IOException ioe)
        {
            throw (new SocksException(SOCKS_PROXY_IO_ERROR, "" + ioe));
        }
        return reply;
    }
//Private methods
//===============
//Constants
    public static final int SOCKS_SUCCESS = 0;
    public static final int SOCKS_FAILURE = 1;
    public static final int SOCKS_BADCONNECT = 2;
    public static final int SOCKS_BADNETWORK = 3;
    public static final int SOCKS_HOST_UNREACHABLE = 4;
    public static final int SOCKS_CONNECTION_REFUSED = 5;
    public static final int SOCKS_TTL_EXPIRE = 6;
    public static final int SOCKS_CMD_NOT_SUPPORTED = 7;
    public static final int SOCKS_ADDR_NOT_SUPPORTED = 8;
    public static final int SOCKS_NO_PROXY = 1 << 16;
    public static final int SOCKS_PROXY_NO_CONNECT = 2 << 16;
    public static final int SOCKS_PROXY_IO_ERROR = 3 << 16;
    public static final int SOCKS_AUTH_NOT_SUPPORTED = 4 << 16;
    public static final int SOCKS_AUTH_FAILURE = 5 << 16;
    public static final int SOCKS_JUST_ERROR = 6 << 16;
    public static final int SOCKS_DIRECT_FAILED = 7 << 16;
    public static final int SOCKS_METHOD_NOTSUPPORTED = 8 << 16;
    static final int SOCKS_CMD_CONNECT = 0x1;
    static final int SOCKS_CMD_BIND = 0x2;
    static final int SOCKS_CMD_UDP_ASSOCIATE = 0x3;
}
